<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h3 data-lake-id="lr9Wt" id="lr9Wt"><span data-lake-id="ub28b2417" id="ub28b2417">背景</span></h3>
  <p data-lake-id="u943ee05b" id="u943ee05b"><span data-lake-id="u44fa6807" id="u44fa6807">假设我们有一个在线游戏平台，需要为每个游戏的玩家实现排行榜功能，以显示每个游戏的最高得分排名。</span></p>
  <p data-lake-id="uf2c434bb" id="uf2c434bb"><span data-lake-id="u10a9dc37" id="u10a9dc37">​</span><br></p>
  <h3 data-lake-id="Od8Ea" id="Od8Ea"><span data-lake-id="u96fbb606" id="u96fbb606">技术选型</span></h3>
  <p data-lake-id="u7a238122" id="u7a238122"><span data-lake-id="ud6143f71" id="ud6143f71">​</span><br></p>
  <p data-lake-id="u22ddd743" id="u22ddd743"><span data-lake-id="u241e425e" id="u241e425e">在这个场景中，主要就是实现排行榜，对于排行榜，有很多种。</span></p>
  <p data-lake-id="u35c7a377" id="u35c7a377"><span data-lake-id="u518540ac" id="u518540ac">​</span><br></p>
  <p data-lake-id="u1883c8bf" id="u1883c8bf"><span data-lake-id="u287df14b" id="u287df14b">有周榜、日榜、小时榜以及分钟榜等，不同的排行榜可以采用不同的技术方案实现。</span></p>
  <p data-lake-id="u4f8dd6e0" id="u4f8dd6e0"><span data-lake-id="u289f6e24" id="u289f6e24">​</span><br></p>
  <p data-lake-id="u309211ce" id="u309211ce"><span data-lake-id="u7c24efb4" id="u7c24efb4">对于周榜、日榜、小时榜等，完全可以基于定时任务、离线任务等生成，直接统计数据然后做排序就行了，难度不大。</span></p>
  <p data-lake-id="u153fbe23" id="u153fbe23"><span data-lake-id="u6cd3633d" id="u6cd3633d">​</span><br></p>
  <p data-lake-id="u045307ee" id="u045307ee"><span data-lake-id="u542ddd4c" id="u542ddd4c">难度比较大的就是分钟级或者秒级的榜单的实现。</span></p>
  <p data-lake-id="u882122de" id="u882122de"><span data-lake-id="uca4ce8ec" id="uca4ce8ec">​</span><br></p>
  <p data-lake-id="u3413dde0" id="u3413dde0"><span data-lake-id="u58531f70" id="u58531f70">对于这种榜单，比较好的办法就是脱离数据库，直接用redis来实现，因为数据库的话大量数据的order by会性能很差。</span></p>
  <p data-lake-id="u9ec8c83b" id="u9ec8c83b"><span data-lake-id="u48f7afed" id="u48f7afed">​</span><br></p>
  <p data-lake-id="ub6e0aacc" id="ub6e0aacc"><span data-lake-id="uf80da958" id="uf80da958">借助redis的zset我们可以非常方便的实现排行榜的功能。</span></p>
  <p data-lake-id="u4dcd4d6c" id="u4dcd4d6c"><br></p>
  <h3 data-lake-id="EutjZ" id="EutjZ"><span data-lake-id="ud2040131" id="ud2040131">你做了什么</span></h3>
  <p data-lake-id="ue3d036d4" id="ue3d036d4"><br></p>
  <p data-lake-id="u4d125b54" id="u4d125b54"><span data-lake-id="u0074f258" id="u0074f258">zset允许我们将每个玩家的得分作为score存储，并使用玩家的唯一标识符作为成员，利用zset自身的基于score排序的功能，我们就能很快实现这个排行榜的功能。</span></p>
  <p data-lake-id="ua4031458" id="ua4031458"><span data-lake-id="ud922d9db" id="ud922d9db">​</span><br></p>
  <p data-lake-id="u0b0dafea" id="u0b0dafea"><img src="https://cdn.nlark.com/yuque/0/2023/png/5378072/1704008013669-eba27ec3-8392-4fd8-bebb-314f4f00f6c7.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_28%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u0367c699" id="u0367c699"><br></p>
  <p data-lake-id="u290129cd" id="u290129cd"><span data-lake-id="u8a557c0f" id="u8a557c0f">借助Redis，我们可以实现以下几个方法：</span></p>
  <p data-lake-id="u3769bcaf" id="u3769bcaf"><span data-lake-id="uf8e633ca" id="uf8e633ca">​</span><br></p>
  <p data-lake-id="u2e08fb75" id="u2e08fb75"><span data-lake-id="u5a4279cd" id="u5a4279cd">1、新用户加入排名：</span></p>
  <p data-lake-id="uef545df0" id="uef545df0"><span data-lake-id="ua5c05099" id="ua5c05099">​</span><br></p>
  <pre lang="java"><code>
// 新用户加入排名
private static void joinLeaderboard(Jedis jedis, String user, double score) {
    jedis.zadd("game:1_ranking", score, user);
}
</code></pre>
  <p data-lake-id="uc72a3ae6" id="uc72a3ae6"><br></p>
  <blockquote data-lake-id="u7fb3b4bc" id="u7fb3b4bc">
   <p data-lake-id="ub1000e5e" id="ub1000e5e"><span data-lake-id="u6a4014cb" id="u6a4014cb" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">zadd 命令用于将一个或多个成员元素及其分数值加入到有序集当中。</span></p>
   <p data-lake-id="u4e827a75" id="u4e827a75"><span data-lake-id="u8a63b016" id="u8a63b016" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">如果某个成员已经是有序集的成员，那么更新这个成员的分数值，并通过重新插入这个成员元素，来保证该成员在正确的位置上。</span></p>
   <p data-lake-id="u0974f6ae" id="u0974f6ae"><span data-lake-id="u5c215ae6" id="u5c215ae6" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">分数值可以是整数值或双精度浮点数</span></p>
   <p data-lake-id="u40b59d73" id="u40b59d73"><span data-lake-id="u878be153" id="u878be153" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">如果有序集合 key 不存在，则创建一个空的有序集并执行 ZADD 操作。</span></p>
  </blockquote>
  <p data-lake-id="uece3f881" id="uece3f881"><br></p>
  <p data-lake-id="ub1ca78c2" id="ub1ca78c2"><span data-lake-id="uaa8df0f6" id="uaa8df0f6">2、用户积分增加：</span></p>
  <p data-lake-id="u26eb6178" id="u26eb6178"><br></p>
  <pre lang="java"><code>
// 用户积分增加
private static void increaseUserScore(Jedis jedis, String user, double score) {
    jedis.zincrby("game:1_ranking", score, user);
}
</code></pre>
  <p data-lake-id="u339a82c6" id="u339a82c6"><br></p>
  <blockquote data-lake-id="u79ebad8a" id="u79ebad8a">
   <p data-lake-id="ue33f3b8e" id="ue33f3b8e"><span data-lake-id="ua5c19f98" id="ua5c19f98" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">zincrby 命令对有序集合中指定成员的分数加上增量 </span></p>
   <p data-lake-id="u235cb530" id="u235cb530"><span data-lake-id="u2682d088" id="u2682d088" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">可以通过传递一个负数值 ，让分数减去相应的值，比如 ZINCRBY key -5 member ，就是让 member 的 score 值减去 5 。</span></p>
   <p data-lake-id="u24c640a8" id="u24c640a8"><span data-lake-id="ub65ce87b" id="ub65ce87b" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">当 key 不存在，或分数不是 key 的成员时， ZINCRBY key increment member 等同于 ZADD key increment member 。</span></p>
   <p data-lake-id="u2aaf10a7" id="u2aaf10a7"><span data-lake-id="u8d88dd6d" id="u8d88dd6d" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">当 key 不是有序集类型时，返回一个错误。</span></p>
   <p data-lake-id="u9be0ec85" id="u9be0ec85"><span data-lake-id="u0f816265" id="u0f816265" class="lake-fontsize-11" style="color: rgb(51, 51, 51)">分数值可以是整数值或双精度浮点数。</span></p>
  </blockquote>
  <p data-lake-id="uefe028e6" id="uefe028e6"><br></p>
  <p data-lake-id="u506a9032" id="u506a9032"><br></p>
  <p data-lake-id="u20ce3612" id="u20ce3612"><span data-lake-id="u85670385" id="u85670385">3、获取前N名玩家：</span></p>
  <p data-lake-id="u5dac101a" id="u5dac101a"><span data-lake-id="u5cb621a4" id="u5cb621a4">​</span><br></p>
  <pre lang="java"><code>
// 获取前N名玩家
private static Set&lt;Tuple&gt; getTopPlayers(Jedis jedis, int n) {
    return jedis.zrevrangeWithScores("game:1_ranking", 0, n - 1);
}
</code></pre>
  <p data-lake-id="u00e7ef2a" id="u00e7ef2a"><br></p>
  <blockquote data-lake-id="uf970debd" id="uf970debd">
   <p data-lake-id="u3df96385" id="u3df96385"><span data-lake-id="ude8f137a" id="ude8f137a">zrevrangeWithScores是Jedis提供的一个方法，是按照分值排序取出后N名。</span></p>
  </blockquote>
  <p data-lake-id="u101ab326" id="u101ab326"><br></p>
  <p data-lake-id="u3727229f" id="u3727229f"><span data-lake-id="ua8edaddb" id="ua8edaddb">有了这三个方法，我们就可以根据需要调用这些函数来实现用户积分的增加、新用户的加入排名和获取前十名玩家的功能。</span></p>
  <p data-lake-id="uc0edece6" id="uc0edece6"><span data-lake-id="u044490ce" id="u044490ce">​</span><br></p>
  <h3 data-lake-id="zzLIg" id="zzLIg"><span data-lake-id="u81c4f9e3" id="u81c4f9e3">进一步优化</span></h3>
  <p data-lake-id="uacfe4920" id="uacfe4920"><br></p>
  <p data-lake-id="u5fd250c2" id="u5fd250c2"><span data-lake-id="u91e7921d" id="u91e7921d">如果，现在我想实现一个新的功能，当用户的积分一样时，按照用户的创建时间来排序，也就是说，如果两个人分数相同，1.1日开通的用户必1.2日开通的用户排名靠前。</span></p>
  <p data-lake-id="ufc9c926b" id="ufc9c926b"><span data-lake-id="u49c528b1" id="u49c528b1">​</span><br></p>
  <p data-lake-id="u226af165" id="u226af165"><span data-lake-id="uadb838b4" id="uadb838b4">要实现这个功能，不要按照分数查询之后再排序，太麻烦了，直接用redis就能干。</span></p>
  <p data-lake-id="u5670c3be" id="u5670c3be"><span data-lake-id="uef76841b" id="uef76841b">​</span><br></p>
  <p data-lake-id="ud2329da6" id="ud2329da6"><span data-lake-id="ufb1f2d27" id="ufb1f2d27">为了实现分数相同按照时间顺序排序，</span><strong><span data-lake-id="u904dde25" id="u904dde25">我们可以将分数score设置为一个浮点数，其中整数部分为积分，小数部分为开通的时间戳</span></strong><span data-lake-id="uec107271" id="uec107271">，如下所示：</span></p>
  <p data-lake-id="udb330225" id="udb330225"><br></p>
  <blockquote data-lake-id="ucfb37cc4" id="ucfb37cc4">
   <p data-lake-id="ub5bc3224" id="ub5bc3224"><span data-lake-id="uddab9abd" id="uddab9abd">score = 分数 + 1-时间戳/1e13</span></p>
  </blockquote>
  <p data-lake-id="u69b5baa8" id="u69b5baa8"><br></p>
  <p data-lake-id="uc30d423b" id="uc30d423b"><span data-lake-id="ubb5c2b48" id="ubb5c2b48">这样可以保证分数相同时，按照时间戳从小到大排序，即先得分的先被排在前面。</span></p>
  <p data-lake-id="u7db7e70c" id="u7db7e70c"><span data-lake-id="ucf12ad9e" id="ucf12ad9e">​</span><br></p>
  <p data-lake-id="u9b1ef436" id="u9b1ef436"><span data-lake-id="u1d6a2320" id="u1d6a2320">我们只需要在用户初始化时，把这部分加进去就行了：</span></p>
  <p data-lake-id="u75060e5d" id="u75060e5d"><span data-lake-id="uc31f3c1f" id="uc31f3c1f">​</span><br></p>
  <pre lang="java"><code>
// 新用户加入排名
private static void joinLeaderboard(Jedis jedis, String user, double score, long openTimestamp) {
    double final_score = score + 1 - timestamp / 1e13;
    jedis.zadd("game:1_ranking", final_score, user);
}
</code></pre>
  <p data-lake-id="u6772625c" id="u6772625c"><br></p>
  <p data-lake-id="ua51322c5" id="ua51322c5"><br></p>
  <h3 data-lake-id="rXUcm" id="rXUcm"><span data-lake-id="u17d7fcfc" id="u17d7fcfc">学习资料</span></h3>
  <p data-lake-id="u7807e9ad" id="u7807e9ad"><br></p>
  <p data-lake-id="u5f1f89c3" id="u5f1f89c3"><br></p>
  <p data-lake-id="u4e094c55" id="u4e094c55"><br></p>
 </body>
</html>